'''
    @project: Masdevallia
    @author: Austin Baker
    @created: 2/22/2014
    @license: http://opensource.org/licenses/mit-license.php

    The MIT License (MIT)

    Copyright (c) 2014 Austin Baker
    
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
    
    About:
    Class for creating and manipulating curve objects.
'''

'''
============================================================
--->>   IMPORT MODULES
============================================================
'''
import maya.OpenMaya as OpenMaya

import masdevallia.utility.Dag as Dag
import masdevallia.nodes.Node as Node
import masdevallia.utility.XML as XML
import masdevallia.utility.MScriptUtility as MScriptUtility

'''
============================================================
--->>   CREATION METHODS
============================================================
'''
def createCircle( inName=None, inNormal=[0,1,0], inRadius=1 ):
    '''
    Creates a nurbs circle.
    
    @param inName: String. Name of curve circle.
    @param inNormal: List. Direction the curve faces. [X,Y,Z]
    @param inRadius: Int. Radius of the circle.
    @return. Node. Curve circle.
    '''
    if not isinstance( inName, str ):
        
        OpenMaya.MGlobal.displayError( 'inName keyword must be a string.' )
        return
    
    if not isinstance( inNormal, list ):
        
        OpenMaya.MGlobal.displayError( 'inNormal keyword must be a list.' )
        return
        
    if not isinstance( inRadius, int ):
        
        OpenMaya.MGlobal.displayError( 'inRadius keyword must be an int.' )
        return
    
    cmd = 'circle -name {0} -normal {1} {2} {3} -radius {4}'.format( inName, inNormal[0], inNormal[1], inNormal[2], inRadius )
    OpenMaya.MGlobal.executeCommand( cmd )
    
    return Node.Node( inName )
    
def createCurve( inName, inShapeName, inCurveData=None, inDegree=1, inForm=1, inOffset=[0,0,0], inScale=1 ):
    '''
    @param inName: String. Name of the curve to be created.
    @param inShapeName: String. Name of the kNurbsCurve.
    @param inCurves: List of Lists. Each index is a CV containing a list of XYZ values for the CV.
    @param inDegree: Int. Degree of the curve. 1, 2, 3, 5, 7
    @param inForm: Int. Open or closed curve. 0 for open, 1 for closed or 2 for periodic.
    @param inOffset: List. Offset for each axis of the curve.
    @return: MObject. Nurbs curve.
    '''    
    if inForm == 0:
        form = OpenMaya.MFnNurbsCurve.kOpen
    elif inForm == 1:
        form = OpenMaya.MFnNurbsCurve.kClosed
    elif inForm == 2:
        form = OpenMaya.MFnNurbsCurve.kPeriodic
    
    for key in inCurveData.keys():
        # Build the knots.
        if key == 'knot':
            knots_array = MScriptUtility.createDoubleArray( inCurveData[key] )
            knots_double_array = OpenMaya.MDoubleArray( knots_array[0], len(inCurveData[key]) )
        
        # Build the CV data into MPoints for the MPointArray.
        if key == 'cv':
            cv_array = OpenMaya.MPointArray()
            for cv in inCurveData[key].keys():
                cv_point = OpenMaya.MPoint( (inCurveData[key][cv][0] * inScale) + inOffset[0],
                                            (inCurveData[key][cv][1] * inScale) + inOffset[1],
                                            (inCurveData[key][cv][2] * inScale) + inOffset[2] )
                cv_array.append( cv_point )
    
    # Create the curve.
    curveFn = OpenMaya.MFnNurbsCurve()
    curve = curveFn.create( cv_array, knots_double_array, inDegree, form, False, False )    
    
    # Set the curve's name.
    MFnDagNode = OpenMaya.MFnDagNode()
    MFnDagNode.setObject( curve )
    MFnDagNode.setName( inName )
    
    #self.name = inName
    #self.fullname = Dag.getPathname( self.name, inFull=True )
    
    # Set the curve's color.
    #self.setColor()
    
    return curve        

def createCompoundCurve( inCurves=None ):
    '''
    Merges curve shapes into one transform node. The final transform node
    is that of the first curve in the list of curves.
    
    @param inCurves: List. Strings of curve names or MObjects of curves.
    @param inRelative: Bool. If True then maintain the curve shapes world
        position relative to the final transform node.
    @return: Node Class.
    '''
    if isinstance( inCurves, list ):
        # Put each curve to a Node class.
        curvesNodes = [] 
        
        for crv in inCurves:
            if isinstance( crv, str ) or isinstance( crv, OpenMaya.MObject ):
                curvesNodes.append( Node.Node( crv ) )
        
        if curvesNodes:
            # Each curve will be moved to the transform node of the first curve
            # in the list.
            transformNode = curvesNodes[0]
            
            for curveNode in curvesNodes:
                curveDagPath = curveNode.asDagPath()
                
                for child in xrange( curveDagPath.childCount() ):
                    childMObject = curveDagPath.child( child )
                    childNode = Node.Node( childMObject )
                    childDagPath = childNode.asDagPath()
                    
                    if childDagPath.apiType() == OpenMaya.MFn.kNurbsCurve: 
                        # We only do reparenting on curves not already a child
                        # of the transformNode.
                        childParent = childNode.getParent()
                        
                        if childParent.name != transformNode.name:
                            # Do the final parenting of the curve shape to the
                            # target transform node.
                            dagMod = OpenMaya.MDagModifier()
                            dagMod.reparentNode( childNode.asMObject(), transformNode.asMObject() )
                            dagMod.doIt()
            
            # Delete the remaining transfrom nodes.
            for nodeIDX in xrange( 1, len( curvesNodes ) ):
                OpenMaya.MGlobal.deleteNode( curvesNodes[nodeIDX].asMObject() )
            
            # Return a Node of the final curve.
            return transformNode
        
    else:
        
        error = 'CurveControl.createCompoundCurve()\n'
        error += '    inCurves keyword is not a list. Got type {0} instead.'.format( type( inCurves ) )
        OpenMaya.MGlobal.displayError( error )


'''
============================================================
--->>   GET/SET METHODS
============================================================
'''
def getCurveCVs( inCurveName ):
    '''
    
    NEED TO CHANGE HOW THIS FUNCTION GETS THE SHAPES FOR THE KTRANSFORM.
    MDAGPATH.CHILDCOUNT() WON'T WORK AS IT ALSO RETURNS ANY KTRANSFORM CHILDREN
    THE CURVE MAY HAVE, NOT JUST SHAPES.
    
    
    Retrieves the positions for all CVs on the curve.
    
    @param inCurveName: String. Name of the object.        
    @return: Dict of List. Each dict holds the information for one CV.
    '''            
    curve_dag = Dag.getDagPath( inCurveName )
    
    fnNurbCurve = OpenMaya.MFnNurbsCurve()
    fnNurbCurve.setObject( curve_dag )
    
    cvsCurve = OpenMaya.MPointArray()
    fnNurbCurve.getCVs( cvsCurve )
    
    results = {}
    for cv in xrange( cvsCurve.length() ):
        results[cv] = [cvsCurve[cv].x, cvsCurve[cv].y, cvsCurve[cv].z]
        
    return results

def getCurveKnots( inCurveName ):
    '''
    
    NEED TO CHANGE HOW THIS FUNCTION GETS THE SHAPES FOR THE KTRANSFORM.
    MDAGPATH.CHILDCOUNT() WON'T WORK AS IT ALSO RETURNS ANY KTRANSFORM CHILDREN
    THE CURVE MAY HAVE, NOT JUST SHAPES.
    

    Retrieves the knots of the curve.
    
    @param inCurveName: String. Name of the object.
    @return: MDoubleArray.
    '''
    curve_dag = Dag.getDagPath( inCurveName )
    knot_array = OpenMaya.MDoubleArray()
    curveFn = OpenMaya.MFnNurbsCurve()
    curveFn.setObject( curve_dag )
    curveFn.getKnots( knot_array )
    return knot_array

def setCurveCVs( **kwargs ):
    '''
    Sets a curve's CVs to the values from inCVArray.
    
    @param inCurve: String. Name of curve to update.
    @param inCVArray: List of MPointArrays.
    '''
    inCurve = None
    inCVArray = None
    
    if len( kwargs ) == 0:
        
        OpenMaya.MGlobal.displayError( 'No keyword arguments passed into the function' )
        return
    
    if 'inCurve' in kwargs:
        
        inCurve = kwargs['inCurve']
        
    else:
        
        OpenMaya.MGlobal.displayError( 'Need a curve to set CVs onto. None given.' )
        return
    
    if 'inCVArray' in kwargs:
        
        inCVArray = kwargs['inCVArray']
        
    else:
        
        OpenMaya.MGlobal.displayError( 'No CV array given' )
        return            
    
    dagCurve = Dag.getDagPath( inCurve )
    children = dagCurve.childCount()
    
    for child in xrange( children ):
        
        childObj = dagCurve.child( child )
        childDag = Dag.getDagPath( childObj )
        
        if childDag.apiType() == OpenMaya.MFn.kNurbsCurve:
            
            fnNurbCurve = OpenMaya.MFnNurbsCurve( childObj )
            fnNurbCurve.setCVs( inCVArray )
            fnNurbCurve.updateCurve()
            
            
def pointArrayToList( **kwargs ):
    '''
    Converts a MPointArray into a list of points which are a list of XYZ values.
    
    @param inPointArray: MPointArray.
    @return List.
    '''
    if len( kwargs ) == 0:
        
        OpenMaya.MGlobal.displayError( 'inPointArray keyword argument required. Not provided.' )
        return
    
    if 'inPointArray' in kwargs:
        
        inPointArray = kwargs['inPointArray']
        
        if isinstance( inPointArray, OpenMaya.MPointArray ):
            
            pointList = []
            
            for index in xrange( inPointArray.length() ):
                
                pointList.append( [ inPointArray[index][0], inPointArray[index][1], inPointArray[index][2] ] )
                
            return pointList


def buildCVPointArrayList( **kwargs ):
    '''
    Builds a list of MPointArrays from the passed in list of dictionaries. Each
    dictionary represents a curve. This is primarily used when dealing with compound
    curves (transforms with multiple curve shapes).
    
    @param inCVs: List of Dicts. Key(Point)/Value(XYZ). Each dict is a separate curve.
    @return: List of MPointArrays.
    '''
    if 'inCVs' in kwargs:
        
        inCVs = kwargs['inCVs']
        
        if isinstance( inCVs, list ):
            
            curveList = []
            
            for curve in inCVs:
                
                curveList.append( buildCVPointArray( inCVDict=curve ) )
                
            return curveList
        
    elif len( kwargs ) == 0:
        
        OpenMaya.MGlobal.displayError( 'No keyword arguments provided. InCVs is required.' )
        return
    
    else:
        
        OpenMaya.MGlobal.displayError( 'inCVs keyword argument required. Not provided.' )
        return
    

def buildCVPointArray( **kwargs ):
    '''
    Builds a MPointArray from a dictionary.
    
    @param inCVDict: Dictionary. Key(Point)/Value(XYZ).
    @return MPointArray.
    '''
    if 'inCVDict' in kwargs:
        
        inCVDict = kwargs['inCVDict']
        
        if isinstance( inCVDict, dict ):
            
            pointArray = OpenMaya.MPointArray()
            
            for cv in inCVDict:
                
                pointArray.append( OpenMaya.MPoint( inCVDict[cv][0], inCVDict[cv][1], inCVDict[cv][2] ) )
                
            return pointArray
        
    elif len( kwargs ) == 0:
        
        OpenMaya.MGlobal.displayError( 'No keyword arguments provided. inCVDict is required.' )
        return
    
    else:
        
        OpenMaya.MGlobal.displayError( 'inCVs keyword argument required. Not provided.' )
        return

def getCurveShapeDegree( inObj ):
    '''
    Gets the curve shape's degree value.
    
    @param inObj: MObject. Curve shape.
    '''        
    degree_plug = Dag.getPlugByName( inObj, 'degree' )
    return Dag.getPlugValue( degree_plug )

def getCurveShapeForm( inObj ):
    '''
    Gets the curve shape's form value.
    
    @param inObj: MObject. Curve shape.
    '''
    form_plug = Dag.getPlugByName( inObj, 'form' )
    return Dag.getPlugValue( form_plug )
    

'''
============================================================
--->>   XML METHODS
============================================================
'''
def writeCurveXML( inCurve=None, inFilePath=None, inIconPath=None ):
    '''
    Writes curve CVs to an XML file.
    
    XML format is:
        <data>
            <!--E:/maya/scripts/masdevallia/rigging/controls/curve/save_test_curve.xml-->
            <object icon="test_curve" name="nurbsCircle1" type="Curve">
                <elem degree="3" form="2" name="bobShape" type="kNurbsCurve">
                    <attr name="" type="knot">
                        <value name="">-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0</value>
                    </attr>
                    <attr name="" type="cv">
                        <value name="0">9.59647468198e-17,1.56722324978,-1.56722324978</value>
                    </attr>
                </elem>
            </object>
        </data>
    
    @param inCurve: String or MObject. Curve to process.
    @param inFilePath: String. Path + filename + extension of the XML file to
        create.
    @param inIconPath: String. Path + filename + extension of the icon associated
        with this curve.
    '''
    curveNode = None
    
    if isinstance( inCurve, str ) or isinstance( inCurve, OpenMaya.MObject ):
        curveNode = Node.Node( inCurve )
        
    else:
        
        error = 'CurveControl.writeCurveXML()\n'
        error += '    inCurve keyword is not of the correct type. Expecting string, MObject or a list of either.'
        OpenMaya.MGlobal.displayError( error )
    
    if not inFilePath:
        
        OpenMaya.MGlobal.displayError( 'writeCurveXML: inFilePath is none. Pass in a file path.' )            
           
    # Format the curve CV data into a XMLData object.
    XMLData = XML.XMLData()
    
    # Make sure none of the attributes have a value of None otherwise the XML
    # build will fail.
    new_object = XML.XMLObject()
    new_object.name = curveNode.name
    new_object.type = 'Curve'
    
    if inIconPath is not None:
        new_object.icon = inIconPath
    else:
        new_object.icon = ''
        
    # Build an element entry for each shape under the kTransform.
    shapes = Dag.getShapes( curveNode.fullName )
    for shape_obj in shapes:
        shape_node = Node.Node( shape_obj )
                
        # Build the XML element (Curve).
        shape_element = XML.XMLCurveElement()
        shape_element.name = shape_node.name
        shape_element.type = 'kNurbsCurve'
        
        # We will run into problems here if there are two shapes with
        # the same name in the scene. Need to come up with a better way
        # to get these values.
        shape_element.degree = getCurveShapeDegree( shape_node.asMObject() )
        shape_element.form = getCurveShapeForm( shape_node.asMObject() )
        
        # Get the curve shape knots.
        shape_knots = getCurveKnots( shape_node.name )
        shape_knots_list = MScriptUtility.getDoubleArrayAsList( shape_knots )
        
        knot_attr = XML.XMLAttribute()
        knot_attr.name = 'knot'
        knot_attr.type = 'knot'
        
        knot_values = XML.XMLValue()
        knot_values.name = ''
        knot_values.text = ','.join( map(str,shape_knots_list) )
        knot_values.type = str( type(shape_knots_list[0]).__name__ )
        
        knot_attr.values.append( knot_values )
        
        shape_element.attributes.append( knot_attr )
        
        # Get the curve shape CVs.
        shape_cvs = getCurveCVs( shape_node.name )
        
        cv_attr = XML.XMLAttribute()
        cv_attr.name = 'cv'
        cv_attr.type = 'cv'
        
        for cv_idx, xyz_values in shape_cvs.items():
            cv_value = XML.XMLValue()
            cv_value.name = str( cv_idx )
            cv_value.text = ','.join( map(str,xyz_values) )
            cv_value.type = str( type(xyz_values[0]).__name__ )
            cv_attr.values.append( cv_value )
                
        shape_element.attributes.append( cv_attr )
            
        # Add the element to the object.
        new_object.elements.append( shape_element )
        
    # Add the object to the XML data.
    XMLData.objects.append( new_object )
    
    # Write the XML file.
    XML.XML().writeFile( inXMLData=XMLData, outFilename=inFilePath )

def readCurveXML( inFilePath=None ):
    '''
    Reads a curve XML file and ????
    
    @param inFilePath: String. File path for the curve XML to read.
    @return: Dict.
    '''
    XMLData = XML.XML().readFile( inFilename=inFilePath )
    
    # Dict where each key, value pair is an object, dict of curve shapes.
    # Each dict of curve shapes has a key, value pair of shape name, curve cv data.
    # curve cv data is a dict of key, value cv id, list of XYZ values.
    curves = {}
    
    for ob in XMLData.objects:
        curve_shapes = {}
        
        for shape_element in ob.elements:
            curve_attrs = {}
            
            for attribute in shape_element.attributes:
                if attribute.name == 'knot':
                    for value in attribute.values:
                        curve_attrs[attribute.name] = map( float, value.text.split( ',' ) )
                
                if attribute.name == 'cv':
                    curve_cvs = {}
                    
                    for value in attribute.values:
                        curve_cvs[value.name] = map( XML.DATA_TYPES[ value.type ], value.text.split( ',' ) )
                        
                    curve_attrs[attribute.name] = curve_cvs
            
            curve_shapes[shape_element.name] = curve_attrs
        
        curves[ob.name] = curve_shapes
                            
    return curves

def getCurveDataFromXMLElement( inXMLElement ):
    '''
    Converts an XMLCurveElement into a list of CV values.
    
    @param inXMLElement: XMLCurveElement Object.
    @return: Dict. Curve knot values and CV XYZ values.
    '''
    results = {}
    for attribute in inXMLElement.attributes:
        # Knots
        if attribute.name == 'knot':
            for value in attribute.values:
                results[attribute.name] = map( float, value.text.split( ',' ) )
            
        if attribute.name == 'cv':
            curve_cvs = {}
                    
            for value in attribute.values:
                curve_cvs[ int(value.name) ] = map( float, value.text.split( ',' ) )
                
            results[attribute.name] = curve_cvs

    return results